/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the  "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id$
 */
package wx.xml.xalan.xalan.templates;

import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;

import wx.xml.xalan.xalan.res.XSLMessages;
import wx.xml.xalan.xalan.res.XSLTErrorResources;
import wx.xml.xalan.xml.serializer.OutputPropertiesFactory;
import wx.xml.xalan.xml.serializer.OutputPropertyUtils;
import wx.xml.xalan.xml.utils.FastStringBuffer;
import wx.xml.xalan.xml.utils.QName;

/**
 * This class provides information from xsl:output elements. It is mainly
 * a wrapper for {@link java.util.Properties}, but can not extend that class
 * because it must be part of the {@link wx.xml.xalan.xalan.templates.ElemTemplateElement}
 * heararchy.
 * <p>An OutputProperties list can contain another OutputProperties list as
 * its "defaults"; this second property list is searched if the property key
 * is not found in the original property list.</p>
 *
 * @see <a href="http://www.w3.org/TR/xslt#dtd">XSLT DTD</a>
 * @see <a href="http://www.w3.org/TR/xslt#output">xsl:output in XSLT Specification</a>
 */
public class OutputProperties extends ElemTemplateElement
    implements Cloneable {
    static final long serialVersionUID = -6975274363881785488L;
    /**
     * The output properties.
     *
     * @serial
     */
    private Properties m_properties = null;

    /**
     * Creates an empty OutputProperties with no default values.
     */
    public OutputProperties() {
        this(wx.xml.xalan.xml.serializer.Method.XML);
    }

    /**
     * Creates an empty OutputProperties with the specified defaults.
     *
     * @param defaults the defaults.
     */
    public OutputProperties(Properties defaults) {
        m_properties = new Properties(defaults);
    }

    /**
     * Creates an empty OutputProperties with the defaults specified by
     * a property file.  The method argument is used to construct a string of
     * the form output_[method].properties (for instance, output_html.properties).
     * The output_xml.properties file is always used as the base.
     * <p>At the moment, anything other than 'text', 'xml', and 'html', will
     * use the output_xml.properties file.</p>
     *
     * @param method non-null reference to method name.
     */
    public OutputProperties(String method) {
        m_properties = new Properties(
            OutputPropertiesFactory.getDefaultMethodProperties(method));
    }

    /**
     * Searches for the qname property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key   the property key.
     * @param props the list of properties to search in.
     * @return the value in this property list as a QName value, or false
     * if null or not "yes".
     */
    public static QName getQNameProperty(String key, Properties props) {

        String s = props.getProperty(key);

        if (null != s)
            return QName.getQNameFromString(s);
        else
            return null;
    }

    /**
     * Searches for the list of qname properties with the specified key in
     * the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key   the property key.
     * @param props the list of properties to search in.
     * @return the value in this property list as a vector of QNames, or false
     * if null or not "yes".
     */
    public static Vector getQNameProperties(String key, Properties props) {

        String s = props.getProperty(key);

        if (null != s) {
            Vector           v       = new Vector();
            int              l       = s.length();
            boolean          inCurly = false;
            FastStringBuffer buf     = new FastStringBuffer();

            // parse through string, breaking on whitespaces.  I do this instead
            // of a tokenizer so I can track whitespace inside of curly brackets,
            // which theoretically shouldn't happen if they contain legal URLs.
            for (int i = 0; i < l; i++) {
                char c = s.charAt(i);

                if (Character.isWhitespace(c)) {
                    if (!inCurly) {
                        if (buf.length() > 0) {
                            QName qname = QName.getQNameFromString(buf.toString());
                            v.addElement(qname);
                            buf.reset();
                        }
                        continue;
                    }
                } else if ('{' == c)
                    inCurly = true;
                else if ('}' == c)
                    inCurly = false;

                buf.append(c);
            }

            if (buf.length() > 0) {
                QName qname = QName.getQNameFromString(buf.toString());
                v.addElement(qname);
                buf.reset();
            }

            return v;
        } else
            return null;
    }

    /**
     * Report if the key given as an argument is a legal xsl:output key.
     *
     * @param key non-null reference to key name.
     * @return true if key is legal.
     */
    public static boolean isLegalPropertyKey(String key) {

        return (key.equals(OutputKeys.CDATA_SECTION_ELEMENTS)
            || key.equals(OutputKeys.DOCTYPE_PUBLIC)
            || key.equals(OutputKeys.DOCTYPE_SYSTEM)
            || key.equals(OutputKeys.ENCODING)
            || key.equals(OutputKeys.INDENT)
            || key.equals(OutputKeys.MEDIA_TYPE)
            || key.equals(OutputKeys.METHOD)
            || key.equals(OutputKeys.OMIT_XML_DECLARATION)
            || key.equals(OutputKeys.STANDALONE)
            || key.equals(OutputKeys.VERSION)
            || (key.length() > 0)
            && (key.charAt(0) == '{')
            && (key.lastIndexOf('{') == 0)
            && (key.indexOf('}') > 0)
            && (key.lastIndexOf('}') == key.indexOf('}')));
    }

    /**
     * Creates an empty OutputProperties with the defaults specified by
     * a property file.  The method argument is used to construct a string of
     * the form output_[method].properties (for instance, output_html.properties).
     * The output_xml.properties file is always used as the base.
     * <p>At the moment, anything other than 'text', 'xml', and 'html', will
     * use the output_xml.properties file.</p>
     *
     * @param method non-null reference to method name.
     * @return Properties object that holds the defaults for the given method.
     * @deprecated Use wx.xml.xalan.xml.serializer.OuputPropertiesFactory.
     * getDefaultMethodProperties directly.
     */
    static public Properties getDefaultMethodProperties(String method) {
        return wx.xml.xalan.xml.serializer.OutputPropertiesFactory.getDefaultMethodProperties(method);
    }

    /**
     * Clone this OutputProperties, including a clone of the wrapped Properties
     * reference.
     *
     * @return A new OutputProperties reference, mutation of which should not
     * effect this object.
     */
    public Object clone() {

        try {
            OutputProperties cloned = (OutputProperties) super.clone();

            cloned.m_properties = (Properties) cloned.m_properties.clone();

            return cloned;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setProperty(QName key, String value) {
        setProperty(key.toNamespacedString(), value);
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setProperty(String key, String value) {
        if (key.equals(OutputKeys.METHOD)) {
            setMethodDefaults(value);
        }

        if (key.startsWith(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL))
            key = OutputPropertiesFactory.S_BUILTIN_EXTENSIONS_UNIVERSAL
                + key.substring(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL_LEN);

        m_properties.put(key, value);
    }

    /**
     * Searches for the property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list with the specified key value.
     */
    public String getProperty(QName key) {
        return m_properties.getProperty(key.toNamespacedString());
    }

    /**
     * Searches for the property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list with the specified key value.
     */
    public String getProperty(String key) {
        if (key.startsWith(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL))
            key = OutputPropertiesFactory.S_BUILTIN_EXTENSIONS_UNIVERSAL
                + key.substring(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL_LEN);
        return m_properties.getProperty(key);
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setBooleanProperty(QName key, boolean value) {
        m_properties.put(key.toNamespacedString(), value ? "yes" : "no");
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setBooleanProperty(String key, boolean value) {
        m_properties.put(key, value ? "yes" : "no");
    }

    /**
     * Searches for the boolean property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>false</code> if the property is not found, or if the value is other
     * than "yes".
     *
     * @param key the property key.
     * @return the value in this property list as a boolean value, or false
     * if null or not "yes".
     */
    public boolean getBooleanProperty(QName key) {
        return getBooleanProperty(key.toNamespacedString());
    }

    /**
     * Searches for the boolean property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>false</code> if the property is not found, or if the value is other
     * than "yes".
     *
     * @param key the property key.
     * @return the value in this property list as a boolean value, or false
     * if null or not "yes".
     */
    public boolean getBooleanProperty(String key) {
        return OutputPropertyUtils.getBooleanProperty(key, m_properties);
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setIntProperty(QName key, int value) {
        setIntProperty(key.toNamespacedString(), value);
    }

    /**
     * Set an output property.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setIntProperty(String key, int value) {
        m_properties.put(key, Integer.toString(value));
    }

    /**
     * Searches for the int property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>false</code> if the property is not found, or if the value is other
     * than "yes".
     *
     * @param key the property key.
     * @return the value in this property list as a int value, or false
     * if null or not a number.
     */
    public int getIntProperty(QName key) {
        return getIntProperty(key.toNamespacedString());
    }

    /**
     * Searches for the int property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>false</code> if the property is not found, or if the value is other
     * than "yes".
     *
     * @param key the property key.
     * @return the value in this property list as a int value, or false
     * if null or not a number.
     */
    public int getIntProperty(String key) {
        return OutputPropertyUtils.getIntProperty(key, m_properties);
    }

    /**
     * Set an output property with a QName value.  The QName will be turned
     * into a string with the namespace in curly brackets.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setQNameProperty(QName key, QName value) {
        setQNameProperty(key.toNamespacedString(), value);
    }

    /**
     * Reset the default properties based on the method.
     *
     * @param method the method value.
     * @see javax.xml.transform.OutputKeys
     */
    public void setMethodDefaults(String method) {
        String defaultMethod = m_properties.getProperty(OutputKeys.METHOD);

        if ((null == defaultMethod) || !defaultMethod.equals(method)
            // bjm - add the next condition as a hack
            // but it is because both output_xml.properties and
            // output_unknown.properties have the same method=xml
            // for their default. Otherwise we end up with
            // a ToUnknownStream wraping a ToXMLStream even
            // when the users says method="xml"
            //
            || defaultMethod.equals("xml")
            ) {
            Properties savedProps = m_properties;
            Properties newDefaults =
                OutputPropertiesFactory.getDefaultMethodProperties(method);
            m_properties = new Properties(newDefaults);
            copyFrom(savedProps, false);
        }
    }

    /**
     * Set an output property with a QName value.  The QName will be turned
     * into a string with the namespace in curly brackets.
     *
     * @param key   the key to be placed into the property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setQNameProperty(String key, QName value) {
        setProperty(key, value.toNamespacedString());
    }

    /**
     * Searches for the qname property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list as a QName value, or false
     * if null or not "yes".
     */
    public QName getQNameProperty(QName key) {
        return getQNameProperty(key.toNamespacedString());
    }

    /**
     * Searches for the qname property with the specified key in the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list as a QName value, or false
     * if null or not "yes".
     */
    public QName getQNameProperty(String key) {
        return getQNameProperty(key, m_properties);
    }

    /**
     * Set an output property with a QName list value.  The QNames will be turned
     * into strings with the namespace in curly brackets.
     *
     * @param key the key to be placed into the property list.
     * @param v   non-null list of QNames corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setQNameProperties(QName key, Vector v) {
        setQNameProperties(key.toNamespacedString(), v);
    }

    /**
     * Set an output property with a QName list value.  The QNames will be turned
     * into strings with the namespace in curly brackets.
     *
     * @param key the key to be placed into the property list.
     * @param v   non-null list of QNames corresponding to <tt>key</tt>.
     * @see javax.xml.transform.OutputKeys
     */
    public void setQNameProperties(String key, Vector v) {

        int s = v.size();

        // Just an initial guess at reasonable tuning parameters
        FastStringBuffer fsb = new FastStringBuffer(9, 9);

        for (int i = 0; i < s; i++) {
            QName qname = (QName) v.elementAt(i);

            fsb.append(qname.toNamespacedString());
            // Don't append space after last value
            if (i < s - 1)
                fsb.append(' ');
        }

        m_properties.put(key, fsb.toString());
    }

    /**
     * Searches for the list of qname properties with the specified key in
     * the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list as a vector of QNames, or false
     * if null or not "yes".
     */
    public Vector getQNameProperties(QName key) {
        return getQNameProperties(key.toNamespacedString());
    }

    /**
     * Searches for the list of qname properties with the specified key in
     * the property list.
     * If the key is not found in this property list, the default property list,
     * and its defaults, recursively, are then checked. The method returns
     * <code>null</code> if the property is not found.
     *
     * @param key the property key.
     * @return the value in this property list as a vector of QNames, or false
     * if null or not "yes".
     */
    public Vector getQNameProperties(String key) {
        return getQNameProperties(key, m_properties);
    }

    /**
     * This function is called to recompose all of the output format extended elements.
     *
     * @param root non-null reference to the stylesheet root object.
     */
    public void recompose(StylesheetRoot root)
        throws TransformerException {
        root.recomposeOutput(this);
    }

    /**
     * This function is called after everything else has been
     * recomposed, and allows the template to set remaining
     * values that may be based on some other property that
     * depends on recomposition.
     */
    public void compose(StylesheetRoot sroot) throws TransformerException {

        super.compose(sroot);

    }

    /**
     * Get the Properties object that this class wraps.
     *
     * @return non-null reference to Properties object.
     */
    public Properties getProperties() {
        return m_properties;
    }

    /**
     * Copy the keys and values from the source to this object.  This will
     * not copy the default values.  This is meant to be used by going from
     * a higher precedence object to a lower precedence object, so that if a
     * key already exists, this method will not reset it.
     *
     * @param src non-null reference to the source properties.
     */
    public void copyFrom(Properties src) {
        copyFrom(src, true);
    }

    /**
     * Copy the keys and values from the source to this object.  This will
     * not copy the default values.  This is meant to be used by going from
     * a higher precedence object to a lower precedence object, so that if a
     * key already exists, this method will not reset it.
     *
     * @param src                 non-null reference to the source properties.
     * @param shouldResetDefaults true if the defaults should be reset based on
     *                            the method property.
     */
    public void copyFrom(Properties src, boolean shouldResetDefaults) {

        Enumeration keys = src.keys();

        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();

            if (!isLegalPropertyKey(key))
                throw new IllegalArgumentException(XSLMessages.createMessage(XSLTErrorResources.ER_OUTPUT_PROPERTY_NOT_RECOGNIZED, new Object[]{key})); //"output property not recognized: "

            Object oldValue = m_properties.get(key);
            if (null == oldValue) {
                String val = (String) src.get(key);

                if (shouldResetDefaults && key.equals(OutputKeys.METHOD)) {
                    setMethodDefaults(val);
                }

                m_properties.put(key, val);
            } else if (key.equals(OutputKeys.CDATA_SECTION_ELEMENTS)) {
                m_properties.put(key, (String) oldValue + " " + (String) src.get(key));
            }
        }
    }

    /**
     * Copy the keys and values from the source to this object.  This will
     * not copy the default values.  This is meant to be used by going from
     * a higher precedence object to a lower precedence object, so that if a
     * key already exists, this method will not reset it.
     *
     * @param opsrc non-null reference to an OutputProperties.
     */
    public void copyFrom(OutputProperties opsrc)
        throws TransformerException {
        // Bugzilla 6157: recover from xsl:output statements
        // checkDuplicates(opsrc);
        copyFrom(opsrc.getProperties());
    }
}
